Spring JDBC

@VERO
Created Date · 2023년 04월 11일 10:04
Last Updated Date · 2023년 04월 11일 10:04

내용을 주관적으로 덜어냈습니다. 자세한 내용은 참고 링크를 확인해주세요! 👍

JDBCTemplate

Querying Dao

  • 하나의 객체를 query할 때 사용한다.
    • 첫 번째 인자: sql, 두 번째 인자: 리턴 타입
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
  • 하나의 객체를 query할 때 with placeholder (?가 placeholder임)
    • 첫 번째 인자: sql, 두 번째 인자: 리턴 타입, 세 번째 인자: ?에 들어갈 값
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
        "select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
  • query 결과가 필드 여러 개일 때
    • sql, mapper, ?에 들어갈 값
Actor actor = jdbcTemplate.queryForObject(
        "select first_name, last_name from t_actor where id = ?",
        (resultSet, rowNum) -> {
            Actor newActor = new Actor();
            newActor.setFirstName(resultSet.getString("first_name"));
            newActor.setLastName(resultSet.getString("last_name"));
            return newActor;
        },
        1212L);
  • query 결과 행이 여러 개일 때
    • sql, mapper, (?에 들어갈 값)
List<Actor> actors = this.jdbcTemplate.query(
        "select first_name, last_name from t_actor",
        (resultSet, rowNum) -> {
            Actor actor = new Actor();
            actor.setFirstName(resultSet.getString("first_name"));
            actor.setLastName(resultSet.getString("last_name"));
            return actor;
        });
  • RowMapper를 분리해서 사용할 때
    • sql, mapper, (?에 들어갈 값)
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
    Actor actor = new Actor();
    actor.setFirstName(resultSet.getString("first_name"));
    actor.setLastName(resultSet.getString("last_name"));
    return actor;
};

public List<Actor> findAllActors() {
    return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}

Updating Dao

  • insert, update, delete query를 하나의 메서드로 사용 가능
    • sql, ? 값 가변인자
this.jdbcTemplate.update(
        "insert into t_actor (first_name, last_name) values (?, ?)",
        "Leonor", "Watling");
this.jdbcTemplate.update(
        "update t_actor set last_name = ? where id = ?",
        "Banjo", 5276L);
this.jdbcTemplate.update(
        "delete from t_actor where id = ?",
        Long.valueOf(actorId));

Named Parameter

NamedParameterJdbcTemplate 클래스는 ? 인수만 사용하여 JDBC 문을 프로그래밍하는 것과 달리 이름이 있는 매개변수를 사용하여 JDBC 문을 프로그래밍할 수 있게 한다.

MapSqlParameterSource 를 사용하면 다음과 같이 쓸 수 있다.

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

만약 SQL에 값을 여러 개 넣어야 하는 경우는 addValue() 로 값을 추가해줄 수 있다.

String sql = "SELECT :a + :b";

SqlParameterSource param = new MapSqlParameterSource()
								.addValue("a", 100)
								.addValue("b", 200);

Map을 사용하여 String 형의 매개변수 이름과 값을 NamedParameterJdbcTemplate 에 전달할 수 있다.

Map을 사용하는 방법의 예제이다.

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActorsByFirstName(String firstName) {

    String sql = "select count(*) from T_ACTOR where first_name = :first_name";

    Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters,  Integer.class);
}

** Collections.singletonMap 은 단 하나의 키를 갖는 맵을 나타낼 때 사용한다.

BeanPropertySqlParameterSource 클래스

아래와 같은 Actor 클래스가 있다고 가정하자. getter는 필수적으로 필요하고, Actor의 필드 이름들은 자동으로 카멜 케이스에서 스네이크 케이스로 변환된다.

public class Actor {

    private Long id;
    private String firstName;
    private String lastName;

    public String getFirstName() {
        return this.firstName;
    }

    public String getLastName() {
        return this.lastName;
    }

    public Long getId() {
        return this.id;
    }

    // setters omitted...

}

: 의 뒤에는 Actor 클래스의 필드 이름을 넣는다. (카멜 케이스 그대로) BeanPropertySqlParameterSource 의 인자로 필드 이름과 같은 값을 갖는 객체를 넣어주면 자동으로 JdbcTemplate이 인식하여 쿼리가 수행된다.

// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

public void setDataSource(DataSource dataSource) {
    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

public int countOfActors(Actor exampleActor) {

    // notice how the named parameters match the properties of the above 'Actor' class
    String sql = "select count(*) from T_ACTOR where first_name = :firstName and last_name = :lastName";

    SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);

    return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}

NamedParameterJdbcTemplate 은 JdbcTemplate을 래핑한 클래스이다. 래핑된 JdbcTemplate 에 접근하기 위해서는 getJdbcOperations() 를 사용하여 JdbcTemplate 에 접근할 수 있다.

SimpleJdbcInsert

Data access layer의 초기화 메서드에서 SimpleJdbcInsert 클래스를 인스턴스화 해야 한다. setDataSource() 메서드에서 dataSource를 초기화하고, 테이블 이름을 설정할 수 있다.

SimpleJdbcInsertexecute() 를 실행하기 위해서는 Map 객체를 만들어서 인자로 넘겨준다. Map의 key는 테이블의 column 이름과 정확하게 일치해야 한다.

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<>(3);
        parameters.put("id", actor.getId());
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        insertActor.execute(parameters);
    }

    // ... additional methods
}

Auto-generated key 를 얻는 방법

auto-increment 되는 id가 있는 경우, 다음과 같은 방법을 사용해야 한다.

SimpleJdbcInsert 선언 시 usingGeneratedKeyColumns("auto-increment 되는 컬럼 이름") 을 설정해준다.

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<>(2);
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

executeAndReturnKey 메서드로 auto-increment 된 ID의 값을 가져올 수 있다.

insert에 필요한 column 제한하기

usingColumns 메서드를 사용하면 column 이름 목록을 지정해서 insert할 column을 제한할 수 있다.

public class JdbcActorDao implements ActorDao {

    private SimpleJdbcInsert insertActor;

    public void setDataSource(DataSource dataSource) {
        this.insertActor = new SimpleJdbcInsert(dataSource)
                .withTableName("t_actor")
                .usingColumns("first_name", "last_name")
                .usingGeneratedKeyColumns("id");
    }

    public void add(Actor actor) {
        Map<String, Object> parameters = new HashMap<>(2);
        parameters.put("first_name", actor.getFirstName());
        parameters.put("last_name", actor.getLastName());
        Number newId = insertActor.executeAndReturnKey(parameters);
        actor.setId(newId.longValue());
    }

    // ... additional methods
}

실제로 코드에서는 이렇게 사용했다.

// 데이터 구조
CREATE TABLE GAME  
(  
    id          BIGINT   NOT NULL AUTO_INCREMENT,  
    trial_count INT      NOT NULL,  
    time        DATETIME NOT NULL default current_timestamp,  
    PRIMARY KEY (id)  
);  
  
CREATE TABLE PLAYER  
(  
    id        BIGINT      NOT NULL AUTO_INCREMENT,  
    game_id   BIGINT      NOT NULL,  
    name      VARCHAR(10) NOT NULL,  
    position  INT         NOT NULL,  
    is_winner BOOL     NOT NULL,  
    PRIMARY KEY (id),  
    FOREIGN KEY (game_id) references GAME (id) on update cascade  
);

내가 웹자동차 미션에서 사용했던 코드는 다음과 같다.

@Repository  
public class GameRepository {  
  
    private final SimpleJdbcInsert insertGame;  
  
    public GameRepository(final DataSource dataSource) {  
        this.insertGame = new SimpleJdbcInsert(dataSource)  
                .withTableName("game")  
                .usingColumns("trial_count")  
                .usingGeneratedKeyColumns("id");  
    }  
  
    public long save(final int trialCount) {  
        HashMap<String, Object> parameters = new HashMap<>();  
        parameters.put("trial_count", trialCount);  
        return insertGame.executeAndReturnKey(parameters).longValue();  
    }  
}

time이 default 값을 가지고 있기 때문에 사용하는 column을 넣어주지 않으면 null이 들어가게 된다. time은 not null 이므로, usingColumns 를 사용하지 않으면 예외가 발생한다. default 값을 갖는 column의 경우에는 usingColumns 를 사용하면 필요한 column만 지정할 수 있어 좋다.